package piano;

import javax.sound.sampled.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.*;
import java.io.ByteArrayInputStream;
import java.text.DecimalFormat;

/**
 * Created with IntelliJ IDEA.
 * Description: GUI，频率采样播放声音
 * User: Endstart
 * Date: 2020-05-10
 * Time: 9:31
 */
public class Beeper extends JApplet {
    BeeperPanel bp;

    public void init() {
        bp = new BeeperPanel();
        getContentPane().add(bp);
        validate();

        String sampleRate = getParameter("samplerate");
        if (sampleRate!=null) {
            try {
                int sR = Integer.parseInt(sampleRate);
                bp.setSampleRateBox(sR);
            } catch(NumberFormatException useDefault) {
            }
        }

        String fpw = getParameter("fpw");
        if (fpw!=null) {
            try {
                int fPW = Integer.parseInt(fpw);
                JSlider slider = bp.getFramesPerWavelengthSlider();
                slider.setValue( fPW );
            } catch(NumberFormatException useDefault) {
            }
        }

        boolean harmonic = (getParameter("addharmonic")!=null);
        bp.setAddHarmonic(harmonic);

        bp.setUpSound();

        if ( getParameter("autoloop")!=null ) {
            String loopcount = getParameter("loopcount");
            if (loopcount!=null) {
                try {
                    Integer lC = Integer.parseInt(loopcount);
                    bp.loop( lC.intValue() );
                } catch(NumberFormatException doNotLoop) {
                }
            }
        }
    }

    public void stop() {
        bp.loopSound(false);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame f = new JFrame("Beeper");
                f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
                BeeperPanel BeeperPanel = new BeeperPanel();
                f.setContentPane(BeeperPanel);
                f.pack();
                f.setMinimumSize( f.getSize() );
                f.setLocationByPlatform(true);
                f.setVisible(true);
            }
        });
    }
}

/** The main UI of Beeper. */
class BeeperPanel extends JPanel {

    JComboBox sampleRateBox;
    JSlider framesPerWavelength;
    JLabel frequency;
    JCheckBox harmonic;
    Clip clip;

    DecimalFormat decimalFormat = new DecimalFormat("###00.00");

    BeeperPanel() {
        super(new BorderLayout());
        // Use current OS look and feel.
        try {
            UIManager.setLookAndFeel(
                    UIManager.getSystemLookAndFeelClassName());
            SwingUtilities.updateComponentTreeUI(this);
        } catch (Exception e) {
            e.printStackTrace();
        }
        setPreferredSize(new Dimension(300, 300));

        JPanel options = new JPanel();
        BoxLayout bl = new BoxLayout(options, BoxLayout.Y_AXIS);
        options.setLayout(bl);

        Integer[] rates = {
                new Integer(8000),
                new Integer(11025),
                new Integer(16000),
                new Integer(22050)
        };
        sampleRateBox = new JComboBox(rates);
        sampleRateBox.setToolTipText("Samples per second");
        sampleRateBox.setSelectedIndex(1);
        JPanel pSampleRate = new JPanel(new BorderLayout());
        pSampleRate.setBorder(new TitledBorder("Sample Rate"));
        pSampleRate.add(sampleRateBox);
        sampleRateBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                setUpSound();
            }
        });
        options.add(pSampleRate);

        framesPerWavelength = new JSlider(JSlider.HORIZONTAL, 10, 200, 25);
        framesPerWavelength.setPaintTicks(true);
        framesPerWavelength.setMajorTickSpacing(10);
        framesPerWavelength.setMinorTickSpacing(5);
        framesPerWavelength.setToolTipText("Frames per Wavelength");
        framesPerWavelength.addChangeListener(new ChangeListener() {
            public void stateChanged(ChangeEvent ce) {
                setUpSound();
            }
        });

        JPanel pFPW = new JPanel(new BorderLayout());
        pFPW.setBorder(new TitledBorder("Frames per Wavelength"));

        pFPW.add(framesPerWavelength);
        options.add(pFPW);

        JPanel bottomOption = new JPanel(new BorderLayout(4, 4));
        harmonic = new JCheckBox("Add Harmonic", false);
        harmonic.setToolTipText(
                "Add harmonic to second channel, common.stack.delayStack.one octave up");
        harmonic.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                setUpSound();
            }
        });
        bottomOption.add(harmonic, BorderLayout.WEST);

        frequency = new JLabel();
        bottomOption.add(frequency, BorderLayout.CENTER);

        options.add(bottomOption);

        add(options, BorderLayout.NORTH);

        JPanel play = new JPanel(new BorderLayout(3, 3));
        play.setBorder(new EmptyBorder(4, 4, 4, 4));
        JButton bPlay = new JButton("Code Key");
        bPlay.setToolTipText("Click to make tone!");
        Dimension preferredSize = bPlay.getPreferredSize();
        bPlay.setPreferredSize(new Dimension(
                (int) preferredSize.getWidth(),
                (int) preferredSize.getHeight() * 3));

        // TODO comment out to try KeyListener!
        //bPlay.setFocusable(false);
        bPlay.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent ke) {
                loopSound(true);
            }
        });
        bPlay.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent me) {
                loopSound(true);
            }

            @Override
            public void mouseReleased(MouseEvent me) {
                loopSound(false);
            }
        });
        play.add(bPlay);

        try {
            clip = AudioSystem.getClip();

            final FloatControl control = (FloatControl)
                    clip.getControl(FloatControl.Type.MASTER_GAIN);

            final JSlider volume = new JSlider(
                    JSlider.VERTICAL,
                    (int) control.getMinimum(),
                    (int) control.getMaximum(),
                    (int) control.getValue()
            );
            volume.setToolTipText("Volume of beep");
            volume.addChangeListener(new ChangeListener() {
                public void stateChanged(ChangeEvent ce) {
                    control.setValue(volume.getValue());
                }
            });
            play.add(volume, BorderLayout.EAST);
        } catch (Exception e) {
            e.printStackTrace();
        }

        add(play, BorderLayout.CENTER);

        setUpSound();
    }

    public void loop(int loopcount) {
        if (clip != null) {
            clip.loop(loopcount);
        }
    }

    public void setAddHarmonic(boolean addHarmonic) {
        harmonic.setSelected(addHarmonic);
    }

    /**
     * Provides the slider for determining the # of frames per wavelength,
     * primarily to allow easy adjustment by host classes.
     */
    public JSlider getFramesPerWavelengthSlider() {
        return framesPerWavelength;
    }

    /**
     * Sets the sample rate to common.stack.delayStack.one of the four
     * allowable rates. Is ignored otherwise.
     */
    public void setSampleRateBox(int sR) {
        switch (sR) {
            case 8000:
                sampleRateBox.setSelectedIndex(0);
                break;
            case 11025:
                sampleRateBox.setSelectedIndex(1);
                break;
            case 16000:
                sampleRateBox.setSelectedIndex(2);
                break;
            case 22050:
                sampleRateBox.setSelectedIndex(3);
                break;
            default:
        }
    }

    /**
     * Sets label to current frequency settings.
     */
    public void setFrequencyLabel() {
        float freq = getFrequency();
        if (harmonic.isSelected()) {
            frequency.setText(
                    decimalFormat.format(freq) +
                            "(/" +
                            decimalFormat.format(freq * 2f) +
                            ") Hz");
        } else {
            frequency.setText(decimalFormat.format(freq) + " Hz");
        }
    }

    /**
     * Generate the tone and inform the user of settings.
     */
    public void setUpSound() {
        try {
            generateTone();
            setFrequencyLabel();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Loops the current Clip until PrintLinkList commence false is passed.
     */
    public void loopSound(boolean commence) {
        if (commence) {
            clip.setFramePosition(0);
            clip.loop(Clip.LOOP_CONTINUOUSLY);
        } else {
            clip.stop();
        }
    }

    /**
     * Generates PrintLinkList tone, and assigns it to the Clip.
     */
    public void generateTone() throws LineUnavailableException {
        if (clip != null) {
            clip.stop();
            clip.close();
        } else {
            clip = AudioSystem.getClip();
        }
        boolean addHarmonic = harmonic.isSelected();

        float sampleRate= ((Integer) sampleRateBox.getSelectedItem()).floatValue();
        int intFPW = framesPerWavelength.getValue();

        // oddly, the sound does not loop well for less than
        // around 5 or so, wavelengths
        int wavelengths = 20;
        byte[] buf = new byte[2 * intFPW * wavelengths];
        AudioFormat af = new AudioFormat(
                sampleRate,
                8,  // sample size in bits
                2,  // channels
                true,  // signed
                false  // bigendian
        );

        for (int i = 0; i < intFPW * wavelengths; i++) {
            double angle = ((float) (i * 2) / ((float) intFPW)) * (Math.PI);
            buf[i * 2] = getByteValue(angle);
            if (addHarmonic) {
                buf[(i * 2) + 1] = getByteValue(2 * angle);
            } else {
                buf[(i * 2) + 1] = buf[i * 2];
            }
        }

        try {
            byte[] b = buf;
            AudioInputStream ais = new AudioInputStream(
                    new ByteArrayInputStream(b), af, buf.length / 2);

            clip.open(ais);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Provides the frequency at current settings for
     * sample rate & frames per wavelength.
     */
    public float getFrequency() {
        int intST = (Integer) sampleRateBox.getSelectedItem();
        int intFPW = framesPerWavelength.getValue();

        return (float) intST / (float) intFPW;
    }

    /**
     * Provides the byte value for this point in the sinusoidal wave.
     */
    private static byte getByteValue(double angle) {
        int maxVol = 127;
        return (new Integer((int) Math.round(Math.sin(angle) * maxVol))).byteValue();
    }
}
